#include <jni.h>

#include <stddef.h>

#include <string.h>

#include "sqlite3.h"

#include "tags.h"

#include "core/tokenizer.h"

#include "core/global.h"

#include "core/table.h"

#include "core/record.h"

#include "core/trans.h"

#include "version/version.h"

#include "core/exception.h"

#include "log/log.h"

#ifdef __cplusplus
extern "C" {
#endif

static jclass class_record;

static jmethodID method_record_constructor;

static bool throw_if_sqlite3_exception(JNIEnv* env, int err, const char* op) {
	if (err == SQLITE_OK || err == SQLITE_DONE || err == SQLITE_ROW) {
		return false;
	}

	log_print(ANDROID_LOG_ERROR, TAG_NIM_INDEX_JNI, "op %s err %d", op, err);

	exception_throw_sqlite3(env, err);

	return true;
}

static void throw_param_exception(JNIEnv* env) {
	exception_throw_param(env);
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeInit(JNIEnv* env, jclass clazz) {
//	log_print(ANDROID_LOG_DEBUG, TAG_NIM_INDEX_JNI, "init");

	record_init(env);

	global_init();
}

jlong Java_com_netease_nimlib_fts_index_NIMIndex_nativeOpen(JNIEnv* env, jclass clazz, jstring path, jboolean writeMode) {
	int flags = writeMode ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : (SQLITE_OPEN_READONLY);

	sqlite3* db = NULL;

	const char* sPath = env->GetStringUTFChars(path, NULL);
	int ret = sqlite3_open_v2(sPath, &db, flags, NULL);
    env->ReleaseStringUTFChars(path, sPath);

    // SQLITE_OK
	if (throw_if_sqlite3_exception(env, ret, "open")) {
		return 0;
	}

	tokenizer_init(db);

	ret = version_init(db);
	if (ret != SQLITE_DONE) {
		sqlite3_close(db);
	}

    // SQLITE_DONE
	if (throw_if_sqlite3_exception(env, ret, "open: version init")) {
		return 0;
	}

	ret = table_init(db);
	if (ret != SQLITE_DONE) {
		sqlite3_close(db);
	}

    // SQLITE_DONE
	if (throw_if_sqlite3_exception(env, ret, "open: table init")) {
		return 0;
	}

	return reinterpret_cast<jlong>(db);
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeClose(JNIEnv* env, jclass clazz, jlong conn) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);

	if (db) {
		int ret = sqlite3_close(db);

//		log_print(ANDROID_LOG_DEBUG, TAG_NIM_INDEX_JNI, "close: %d", ret);
	}
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeTransOp(JNIEnv* env, jclass clazz, jlong conn, jint op) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return;
	}

    // SQLITE_DONE
	int ret = trans_op(db, op);

	throw_if_sqlite3_exception(env, ret, "trans op");
}

jlong Java_com_netease_nimlib_fts_index_NIMIndex_nativeGetVersion(JNIEnv* env, jclass clazz, jlong conn, jlong type) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return 0;
	}

	long long ver = 0;
	int ret = version_get(db, type, &ver);

    // SQLITE_OK
	throw_if_sqlite3_exception(env, ret, "get version");

	return ver;
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeSetVersion(JNIEnv* env, jclass clazz, jlong conn, jlong type, jlong ver) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return;
	}

    // SQLITE_DONE
	int ret = version_set(db, type, ver);

	throw_if_sqlite3_exception(env, ret, "set version");
}

jlong Java_com_netease_nimlib_fts_index_NIMIndex_nativeQueryOpen(JNIEnv* env, jclass clazz, jlong conn, jlong type, jstring query, jint limit, jstring id) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return 0;
	}

	sqlite3_stmt* statement = NULL;

	int ret = record_query(db, type, query, limit, id, env, &statement);

    // SQLITE_OK
	throw_if_sqlite3_exception(env, ret, "query open");

	// init
	jclass local_class_record = env->FindClass("com/netease/nimlib/search/model/NIMIndexRecord"); // change local to global
	class_record = (jclass)env->NewGlobalRef(local_class_record);
	env->DeleteLocalRef(local_class_record);

	method_record_constructor = env->GetMethodID(class_record, "<init>", "()V");

    return reinterpret_cast<jlong>(statement);
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeQueryClose(JNIEnv* env, jclass clazz, jlong query) {
	sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(query);

	if (statement) {
		sqlite3_finalize(statement);
	}

	// release global
	env->DeleteGlobalRef(class_record);
}

jboolean Java_com_netease_nimlib_fts_index_NIMIndex_nativeQueryN(JNIEnv* env, jclass clazz, jlong query, jobjectArray outRecords) {
	sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(query);
	if (!statement) {
		throw_param_exception(env);

		return false;
	}

	jsize N = env->GetArrayLength(outRecords);
	jsize i = 0;
	jobject record;
	int ret;
	while(i < N) {
		ret = sqlite3_step(statement);
		if (ret == SQLITE_ROW) {
			record = env->NewObject(class_record, method_record_constructor);
			record_copy(statement, record, env); // set fields
			env->SetObjectArrayElement(outRecords, i++, record);
		} else {
			break;
		}
	}

	// SQLITE_ROW || SQLITE_DONE
	throw_if_sqlite3_exception(env, ret, "query N");

	return ret == SQLITE_ROW;
}

jboolean Java_com_netease_nimlib_fts_index_NIMIndex_nativeQueryNext(JNIEnv* env, jclass clazz, jlong query, jobject record) {
	sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(query);
	if (!statement) {
		throw_param_exception(env);

		return false;
	}

	int ret = sqlite3_step(statement);
	if (ret == SQLITE_ROW) {
		record_copy(statement, record, env);
	}

    // SQLITE_ROW || SQLITE_DONE
	throw_if_sqlite3_exception(env, ret, "query next");

	return ret == SQLITE_ROW;
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeInsert(JNIEnv* env, jclass clazz, jlong conn, jobject record) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return;
	}

	int ret = record_insert(db, record, env);

    // SQLITE_DONE
	throw_if_sqlite3_exception(env, ret, "insert");
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeBulkInsert(JNIEnv* env, jclass clazz, jlong conn, jobjectArray records) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return;
	}

	int ret = record_bulk_insert(db, records, env);

    // SQLITE_DONE
	throw_if_sqlite3_exception(env, ret, "bulk insert");
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeDelete(JNIEnv* env, jclass clazz, jlong conn, jlong type, jlong dataid, jstring id) {
	sqlite3* db = reinterpret_cast<sqlite3*>(conn);
	if (!db) {
		throw_param_exception(env);

		return;
	}

	int ret = record_delete(db, type, dataid, id, env);

    // SQLITE_DONE
	throw_if_sqlite3_exception(env, ret, "delete");
}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeTest(JNIEnv* env, jclass clazz) {

}

void Java_com_netease_nimlib_fts_index_NIMIndex_nativeTestToken(JNIEnv* env, jclass clazz, jstring query) {
	const char* sQuery = env->GetStringUTFChars(query, NULL);
	tokenizer_test(sQuery);
	env->ReleaseStringUTFChars(query, sQuery);
}

#ifdef __cplusplus
}
#endif
